////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Daniel F. Hanson
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Text;

namespace SilverLua.Compiler
{
   internal class Lexer
   {
      private string _script;
      private char _peek;
      private int _index;
      private int _lineIndex;
      private int _columnIndex;
      
      private static readonly Dictionary<string, object> Keywords;
      
      static Lexer()
      {
         Keywords = new Dictionary<string, object>();
         Keywords.Add("and", null);
         Keywords.Add("break", null);
         Keywords.Add("do", null);
         Keywords.Add("else", null);
         Keywords.Add("elseif", null);
         Keywords.Add("end", null);
         Keywords.Add("false", null);
         Keywords.Add("for", null);
         Keywords.Add("function", null);
         Keywords.Add("if", null);
         Keywords.Add("in", null);
         Keywords.Add("local", null);
         Keywords.Add("nil", null);
         Keywords.Add("not", null);
         Keywords.Add("or", null);
         Keywords.Add("repeat", null);
         Keywords.Add("return", null);
         Keywords.Add("then", null);
         Keywords.Add("true", null);
         Keywords.Add("until", null);
         Keywords.Add("while", null);
      }
      
      internal bool IsEOF { get { return _index >= _script.Length; } }

      internal void Load(string script)
      {
         _script = script.Replace("\r\n", "\n");
         _peek = _script[0];
         _index = 0;
         _lineIndex = 0;
         _columnIndex = 0;
      }

      internal Token NextToken(bool allowComments)
      {
         Token token;
         
         do
         {
            token = NextToken();
         } while (!allowComments && token.Type == TokenType.Comment || token.Type == TokenType.WhiteSpace);
         
         return token;
      }
      
      private Token NextToken()
      {
         if (IsEOF) return Token.EOF;
         
         char nextChar = NextChar();
         StringBuilder token = new StringBuilder();
         token.Append(nextChar);
         
         switch (nextChar)
         {
            case '+':
            case '*':
            case '/':
            case '%':
            case '^':
            {
               return new Token(token.ToString(), TokenType.BinaryOperator);
            }
            case '#':
            {
               return new Token(token.ToString(), TokenType.UnaryOperator);
            }
            case '=':
            case '<':
            case '>':
            {
               TokenType type = nextChar == '=' ? TokenType.Punctuation : TokenType.BinaryOperator;
               
               if (!IsEOF && PeekChar() == '=')
               {
                  token.Append(NextChar());
                  type = TokenType.BinaryOperator;
               }
               return new Token(token.ToString(), type);
            }
            case '~':
            {
               if (!IsEOF && PeekChar() == '=')
               {
                  token.Append(NextChar());
                  return new Token(token.ToString(), TokenType.BinaryOperator);
               }
               else
               {
                  throw new Exception("Missing = after ~");
               }
            }
            case '(':
            case ')':
            case '{':
            case '}':
            case ']':
            case ';':
            case ':':
            case ',':
            {
               return new Token(token.ToString(), TokenType.Punctuation);
            }
            case '-':
            {
               if (!IsEOF && PeekChar() == '-')
               {
                  token.Append(NextChar());
                  bool isLongComment = false;

                  if (!IsEOF && PeekChar() == '[')
                  {
                     token.Append(NextChar());
                     if (!IsEOF && PeekChar() == '[')
                     {
                        token.Append(NextChar());
                        isLongComment = true;
                     }
                  }
                  
                  if (isLongComment)
                  {
                     bool firstBracketFound = false;
                     char peek = PeekChar();

                     while (!firstBracketFound || peek != ']')
                     {
                        firstBracketFound = peek == ']';
                        token.Append(NextChar());
                        peek = PeekChar();
                     }
                     
                     token.Append(NextChar());
                  }
                  else
                  {
                     while (!IsEOF && PeekChar() != '\n')
                     {
                        token.Append(NextChar());
                     }
                  }
                  
                  return new Token(token.ToString(), TokenType.Comment);
               }
               else
               {
                  return new Token(token.ToString(), TokenType.UnaryOperator);
               }
            }
            case '[':
            {
               if (!IsEOF && PeekChar() == '[')
               {
                  token.Append(NextChar());
                  if (!IsEOF && PeekChar() == '\n') NextChar();
                  
                  bool firstBracketFound = false;
                  char peek = PeekChar();

                  while (!firstBracketFound || peek != ']')
                  {
                     firstBracketFound = peek == ']';
                     token.Append(NextChar());
                     peek = PeekChar();
                  }
                  
                  return new Token(token.ToString(), TokenType.LongStringLiteral);
               }
               else
               {
                  return new Token(token.ToString(), TokenType.Punctuation);
               }
            }
            case '.':
            {
               TokenType type = TokenType.Punctuation;

               if (!IsEOF && PeekChar() == '.')
               {
                  type = TokenType.BinaryOperator;
                  token.Append(NextChar());
                  if (!IsEOF && PeekChar() == '.')
                  {
                     type = TokenType.Punctuation;
                     token.Append(NextChar());
                  }
               }
               
               return new Token(token.ToString(), type);
            }
            case '\'':
            case '"':
            {
               char quote = nextChar;
               char peek = PeekChar();
               
               while (peek != quote)
               {
                  if (peek == '\n')
                  {
                     throw new Exception("Missing closing quote");
                  }
                  else if (peek == '\\')
                  {
                     NextChar();
                     peek = PeekChar();
                     switch (peek)
                     {
                        case 'a': token.Append('\a'); break;
                        case 'b': token.Append('\b'); break;
                        case 'f': token.Append('\f'); break;
                        case 'n': token.Append('\n'); break;
                        case 'r': token.Append('\r'); break;
                        case 't': token.Append('\t'); break;
                        case 'v': token.Append('\v'); break;
                        case '\\': token.Append('\\'); break;
                        case '\'': token.Append('\''); break;
                        case '"': token.Append('"'); break;
                        case 'u':
                        {
                           NextChar();
                           int hex = 0;
                           for (int i = 0; i < 4; ++i)
                           {
                              peek = PeekChar();
                              hex *= 16;
                              if (char.IsDigit(peek))
                              {
                                 hex += peek - '0';
                              }
                              else if (peek >= 'A' && peek <= 'F')
                              {
                                 hex += 10 + peek - 'A';
                              }
                              else if (peek >= 'a' && peek <= 'f')
                              {
                                 hex += 10 + peek - 'a';
                              }
                              else
                              {
                                 throw new Exception("Invalid hex digit");
                              }
                           }
                           token.Append((char)hex);
                        }
                        break;
                        default:
                        {
                           if (char.IsDigit(peek))
                           {
                              int ascii = NextChar() - '0';
                              int digitCount = 1;
                              while (digitCount < 3 && char.IsDigit(PeekChar()))
                              {
                                 ascii *= 10;
                                 ascii += NextChar()- '0';
                                 ++digitCount;
                              }
                              token.Append((char)ascii);
                           }
                           else
                           {
                              throw new Exception("Invalid escape sequence");
                           }
                        }
                        break;
                     }
                  }
                  else
                  {
                     token.Append(NextChar());
                  }
                  
                  peek = PeekChar();
               }
               
               NextChar();
               
               return new Token(token.ToString(), TokenType.StringLiteral);
            }
            default:
            {
               if (char.IsDigit(nextChar))
               {
                  if (!IsEOF && nextChar == '0' && (PeekChar() == 'X' || PeekChar() == 'x'))
                  {
                     token.Append(NextChar());
                     char peek = PeekChar();
                     
                     while (char.IsDigit(peek) ||
                            peek >= 'a' && peek <= 'f' ||
                            peek >= 'A' && peek <= 'F')
                     {
                        token.Append(peek);
                        peek = PeekChar();
                     }
                     
                     return new Token(token.ToString(), TokenType.HexNumericLiteral);
                  }
                  else
                  {
                     TokenType type = TokenType.NumericLiteral;

                     while (char.IsDigit(PeekChar()))
                     {
                        token.Append(NextChar());
                     }

                     if (PeekChar() == '.')
                     {
                        token.Append(NextChar());

                        while (char.IsDigit(PeekChar()))
                        {
                           token.Append(NextChar());
                        }
                     }

                     char peek = PeekChar();
                     if (peek == 'e' || peek == 'E')
                     {
                        type = TokenType.ScientificNumericLiteral;
                        token.Append(NextChar());

                        if (PeekChar() == '-')
                        {
                           token.Append(NextChar());
                        }

                        if (char.IsDigit(PeekChar()))
                        {
                           while (char.IsDigit(PeekChar()))
                           {
                              token.Append(NextChar());
                           }
                        }
                        else
                        {
                           throw new Exception("Invalid exponent");
                        }
                     }

                     return new Token(token.ToString(), type);
                  }
               }
               else if (char.IsLetter(nextChar) || nextChar == '_')
               {
                  while (!IsEOF && (char.IsLetterOrDigit(PeekChar()) || PeekChar() == '_'))
                  {
                     token.Append(NextChar());
                  }
                  
                  string tokenString = token.ToString();
                  return new Token(tokenString, Keywords.ContainsKey(tokenString) ? TokenType.Keyword : TokenType.Identifier);
               }
               else if (char.IsWhiteSpace(nextChar))
               {
                  while (!IsEOF && char.IsWhiteSpace(PeekChar()))
                  {
                     token.Append(NextChar());
                  }
                  
                  return new Token(token.ToString(), TokenType.WhiteSpace);
               }
               else
               {
                  throw new Exception("Invalid character");
               }
            }
         }
      }
      
      private char PeekChar()
      {
         if (_script == null)
         {
            throw new InvalidOperationException("No script has been loaded into the Lexer");
         }
         else if (_index >= _script.Length)
         {
            throw new InvalidOperationException("Unexpected EOF");
         }

         return _peek;
      }
      
      private char NextChar()
      {
         char c = PeekChar();
         ++_index;
         
         if (c == '\n')
         {
            ++_lineIndex;
            _columnIndex = 0;
         }
         else
         {
            ++_columnIndex;
         }
         
         if (_index < _script.Length)
         {
            _peek = _script[_index];
         }
         
         return c;
      }
   }
}
